# Copyright (c) 2018 Presto Labs Pte. Ltd.
# Author: xguo

import enum
import json
import logging
import time
import typing
import urllib.parse

import jwt
import requests
import requests.auth

from coin.exchange.base.kr_rest.auth_base import AuthKey
from coin.exchange.base.kr_rest.rest_client_base import RestUpdate
from coin.exchange.base.order_gateway import OrderSide, OrderType
from coin.exchange.base.rest.request_nonce_manager import RequestNonceManager
from coin.exchange.util.async_http_client import AsyncHttpClient
from coin.exchange.util.rate_limit import RateLimiter

QUOINEX_API_URL = 'api.quoine.com'
LIQUID_API_URL = 'api.liquid.com'


class QuoinexOrderSide(enum.Enum):
  BUY = 'buy'
  SELL = 'sell'


class QuoinexOrderDirection(enum.Enum):
  ONE_DIRECTION = 'one_direction'
  TWO_DIRECTION = 'two_direction'
  NETOUT = 'net_out'


class QuoinexOrderType(enum.Enum):
  LIMIT = 'limit'
  MARKET = 'market'
  MARKET_WITH_RANGE = 'market_with_range'


class QuoinexLeverageLevel(enum.IntEnum):
  x2 = 2
  x4 = 4
  x5 = 5
  x10 = 10
  x25 = 25


class QuoinexOrderStatus(enum.Enum):
  LIVE = 'live'
  FILLED = 'filled'
  PARTIALLY_FILLED = 'partially_filled'
  CANCELED = 'cancelled'


def to_quoinex_order_side(order_side: OrderSide):
  assert isinstance(order_side, OrderSide), order_side
  return getattr(QuoinexOrderSide, order_side.name)


def from_quoinex_order_side(order_side: QuoinexOrderSide):
  assert isinstance(order_side, QuoinexOrderSide), order_side
  return getattr(OrderSide, order_side.name)


def to_quoinex_order_type(order_type: OrderType):
  assert isinstance(order_type, OrderType), order_type
  return getattr(QuoinexOrderSide, order_type.name)


def from_quoinex_order_type(order_type: QuoinexOrderType):
  assert isinstance(order_type, QuoinexOrderType), order_type
  return getattr(OrderType, order_type.name)


class QuoinexAuth(requests.auth.AuthBase):
  def __init__(self, key_file, rest_request_gap_sec=None):
    self._key = AuthKey.from_file(key_file) if key_file else None
    self._nonce_manager = RequestNonceManager(request_gap_sec=rest_request_gap_sec)

  def __call__(self, req: requests.PreparedRequest):
    parse_result = urllib.parse.urlparse(req.url)
    path = urllib.parse.ParseResult(
        scheme='',
        netloc='',
        path=parse_result.path,
        params=parse_result.params,
        query=parse_result.query,
        fragment='',
    ).geturl()
    nonce = self._nonce_manager.get_nonce()
    auth_payload = {
        "path": path,
        "nonce": nonce,
        "token_id": self._key.access_key,
    }
    signature = jwt.encode(auth_payload, self._key.secret_key, 'HS256')
    headers = {
        'X-Quoine-API-Version': 2,
        'X-Quoine-Auth': signature,
        'Content-Type': 'application/json',
        'HTTP_ACCEPT_LANGUAGE': 'en-us',
    }
    req.headers.update(headers)
    return req


class QuoinexSyncQueryer(object):
  def __init__(self,
               key_file,
               *,
               api_host=None,
               disable_ssl=False,
               rest_request_gap_sec=None,
               timeout=10):
    self._timeout = timeout
    self._rate_limit = RateLimiter(300, 300)
    self._auth = QuoinexAuth(key_file, rest_request_gap_sec)
    scheme = 'http://' if disable_ssl else 'https://'
    self._api_host = scheme + (api_host or LIQUID_API_URL)
    self._logger = logging.getLogger(__name__)

  def auth_request(self, http_method, path, params):
    url = urllib.parse.urljoin(self._api_host, path)
    response = None
    try:
      if http_method == 'GET':
        response = requests.get(url, params=params, auth=self._auth, timeout=self._timeout)
      elif http_method == 'POST':
        response = requests.post(url, json=params, auth=self._auth, timeout=self._timeout)
      elif http_method == 'PUT':
        response = requests.put(url, json=params, auth=self._auth, timeout=self._timeout)
      else:
        raise ValueError('Invalid http method %s' % http_method)
      return response
    except requests.exceptions.RequestException as e:
      self._logger.exception('RequestException: %s', e)
      raise
    except json.JSONDecodeError as e:
      self._logger.exception('JsonDecodeException: %s\n %s', response.content, e)
      raise
    except Exception as e:
      self._logger.exception('Unknown exception, %s', e)
      raise

  def query_nocheck(self, http_method, endpoint, params=None):
    req_timestamp = time.time()
    response = self.auth_request(http_method, endpoint, params)
    res_timestamp = time.time()
    if response.status_code == 200:
      update = RestUpdate(req_timestamp,
                          res_timestamp,
                          response.json(),
                          status=response.status_code)
    else:
      update = RestUpdate(req_timestamp, res_timestamp, response.text, status=response.status_code)
    return update

  def query(self, http_method, path, params=None, disable_rate_limit=False):
    assert http_method in ('GET', 'POST', 'PUT'), http_method
    if not disable_rate_limit:
      while not self._rate_limit.check():
        time.sleep(0.1)
    else:
      self._rate_limit.check()
    return self.query_nocheck(http_method, path, params=params)


class QuoinexAsyncQueryer(object):
  def __init__(
      self,
      key_file,
      *,
      api_host=None,
      disable_ssl=False,
      timeout=10,
      max_connections=8,
      rest_request_gap_sec=None,
  ):
    self._timeout = timeout
    self._http_client = AsyncHttpClient(max_connections=max_connections)
    self._auth = QuoinexAuth(key_file, rest_request_gap_sec)
    scheme = 'http://' if disable_ssl else 'https://'
    self._api_host = scheme + (api_host or LIQUID_API_URL)

  def query(self, http_method, path, params=None):
    assert http_method in ('GET', 'POST', 'PUT'), http_method
    url = urllib.parse.urljoin(self._api_host, path)
    if http_method == 'GET':
      future = self._http_client.get(url, params=params, auth=self._auth, timeout=self._timeout)
    elif http_method == 'POST':
      future = self._http_client.post(url, json=params, auth=self._auth, timeout=self._timeout)
    elif http_method == 'PUT':
      future = self._http_client.put(url, json=params, auth=self._auth, timeout=self._timeout)
    else:
      raise ValueError('Invalid http method %s' % http_method)
    return future


class QuoinexNativePrivateClient(object):
  def __init__(
      self,
      *,
      key_file,
      api_host=None,
      disable_ssl=False,
      timeout=10,
      max_connections=8,
      use_async=False,
      rest_request_gap_sec=None,
  ):
    api_host = api_host or LIQUID_API_URL
    if use_async:
      self._auth_queryer = QuoinexAsyncQueryer(
          key_file,
          api_host=api_host,
          disable_ssl=disable_ssl,
          timeout=timeout,
          max_connections=max_connections,
          rest_request_gap_sec=rest_request_gap_sec,
      )
    else:
      self._auth_queryer = QuoinexSyncQueryer(
          key_file,
          api_host=api_host,
          disable_ssl=disable_ssl,
          timeout=timeout,
          rest_request_gap_sec=rest_request_gap_sec,
      )

  def _query(self, http_method, path, params=None):
    return self._auth_queryer.query(http_method, path, params=params)

  # create order
  def query_create_an_order(
      self,
      *,
      product_id,
      price,
      quantity,
      order_side: QuoinexOrderSide,
      order_type: QuoinexOrderType,
      price_range=None,
      leverage: typing.Union[None, QuoinexLeverageLevel] = None,
      funding_currency=None,  # margin
      order_direction: typing.Union[None, QuoinexOrderDirection] = None,
  ):
    path = 'orders'
    method = 'POST'

    if leverage is not None:
      leverage = leverage.value

    if order_direction is not None:
      order_direction = order_direction.value

    params = {
        "order": {
            'product_id': product_id,
            'price': price,
            'quantity': quantity,
            'side': order_side.value,
            'order_type': order_type.value,
            'price_range': price_range,
            'leverage_level': leverage,
            'funding_currency': funding_currency,
            'order_direction': order_direction,
        }
    }
    update = self._query(method, path, params)
    return update

  def query_get_an_order(self, order_id: int):
    method = 'GET'
    path = 'orders/{id}'.format(id=order_id)
    update = self._query(method, path)
    return update

  def query_get_orders(self,
                       funding_currency=None,
                       product_id=None,
                       status: typing.Union[None, QuoinexOrderStatus] = None,
                       with_details: bool = True,
                       limit=None,
                       page=None):
    assert isinstance(with_details, bool), with_details

    method = 'GET'
    path = 'orders'

    if with_details is True:
      with_details = 1
    else:
      with_details = None

    if status is not None:
      status = status.value

    params = {
        'funding_currency': funding_currency,
        'product_id': product_id,
        'status': status,
        'with_details': with_details,
        'limit': limit,
        'page': page,
    }
    update = self._query(method, path, params)
    return update

  def query_cancel_an_order(self, order_id):
    method = 'PUT'
    path = 'orders/{id}/cancel'.format(id=order_id)
    update = self._query(method, path)
    return update

  def query_amend_an_order(self, order_id, price, quantity):
    method = 'PUT'
    path = 'orders/{id}'.format(id=order_id)
    params = {
        'price': price,
        'quantity': quantity,
    }
    update = self._query(method, path, params)
    return update

  def query_get_order_trades(self, order_id):
    method = 'GET'
    path = 'orders/{id}/trades'.format(id=order_id)
    update = self._query(method, path)
    return update

  def query_get_order_executions(self, order_id):
    method = 'GET'
    path = 'orders/{id}/executions'.format(id=order_id)
    update = self._query(method, path)
    return update

  def query_get_product_executions(self, product_id, limit=None, page=None):
    method = 'GET'
    path = 'executions/me'
    params = {
        'product_id': product_id,
        'limit': limit,
        'page': page,
    }
    update = self._query(method, path, params)
    return update

  def query_get_fiat_accounts(self, currency):
    method = 'POST'
    path = 'fiat_accounts'
    params = {
        'currency': currency,
    }
    update = self._query(method, path, params)
    return update

  def query_get_coin_accounts(self):
    method = 'GET'
    path = 'crypto_accounts'
    update = self._query(method, path)
    return update

  def query_get_all_account_balances(self):
    method = 'GET'
    path = 'accounts/balance'
    update = self._query(method, path)
    return update
